﻿using System;
using System.Linq;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using Microsoft.Xrm.Client.Services;
using Microsoft.Xrm.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using RelationshipType = Sustainalytics.Entities.RelationshipType;

namespace Sustainalytics.DynamicsCRMGateway.Tests
{
    [TestClass]
    public class DynamicsCrmGatewayTests
    {
        private const string CrmAccount = "account";
        private const string CrmOffice = "territory";
        private const string CrmName = "name";
        private const string CrmRelationshipType = "customertypecode";
        private const string CrmAccountOfficeRelationship = "territory_accounts";
        private const string CrmManager = "systemuser";
        private const string CrmManagerEmail = "internalemailaddress";
        private const string CrmAccountManagerRelationship = "system_user_accounts";
        private const string CrmPlatformAccess = "new_platformaccess";

        private const string CrmTestUser = "client.platform@sustainalytics.com";
        private const string ConnectionString = "Url=https://sustainalytics0.crm4.dynamics.com;Username=" + CrmTestUser + ";Password=M46iaHerm3s21!";
        private Guid _crmTestUserId;
        private DateTime _insertionDate;
        private List<CrmEntity> _accounts;
        private List<CrmEntity> _offices;
        private List<CrmEntity> _contacts;


        public TestContext TestContext { get; set; }

        [TestInitialize]
        public void Initialize()
        {
            if (TestContext.TestName == "CRM_Schema_Validation")
                return;
            CreateTestAccounts();
            CreateTestContacts();
        }

        [TestCleanup]
        public void Cleanup()
        {
            if (TestContext.TestName == "CRM_Schema_Validation")
                return;

            DeleteTestContacts();
            DeleteTestAccounts();
        }

        private void CreateTestAccounts()
        {
            _offices = new List<CrmEntity>();
            var office = new CrmEntity(CrmOffice) { Id = Guid.NewGuid() };
            office[CrmName] = "UNIT TEST Office 1 - " + TestContext.TestName;
            _offices.Add(office);

            office = new CrmEntity(CrmOffice) { Id = Guid.NewGuid() };
            office[CrmName] = "UNIT TEST Office 2 - " + TestContext.TestName;
            _offices.Add(office);

            office = new CrmEntity(CrmOffice) { Id = Guid.NewGuid() };
            office[CrmName] = "UNIT TEST Office 3 - " + TestContext.TestName;
            _offices.Add(office);

            office = new CrmEntity(CrmOffice) { Id = Guid.NewGuid() };
            office[CrmName] = "UNIT TEST Office 4 - " + TestContext.TestName;
            _offices.Add(office);

            _accounts = new List<CrmEntity>();
            var account = new CrmEntity(CrmAccount) { Id = Guid.NewGuid() };
            account[CrmName] = "UNIT TEST Account 1 - " + TestContext.TestName;
            account[CrmRelationshipType] = new OptionSetValue((int)RelationshipType.Client);
            _accounts.Add(account);

            account = new CrmEntity(CrmAccount) { Id = Guid.NewGuid() };
            account[CrmName] = "UNIT TEST Account 2 - " + TestContext.TestName;
            account[CrmRelationshipType] = new OptionSetValue((int)RelationshipType.Prospect);
            _accounts.Add(account);

            account = new CrmEntity(CrmAccount) { Id = Guid.NewGuid() };
            account.Id = Guid.NewGuid();
            account[CrmName] = "UNIT TEST Account 3 - " + TestContext.TestName;
            account[CrmRelationshipType] = new OptionSetValue((int)RelationshipType.FormerClient);
            _accounts.Add(account);

            account = new CrmEntity(CrmAccount) { Id = Guid.NewGuid() };
            account.Id = Guid.NewGuid();
            account[CrmName] = "UNIT TEST Account 4 - " + TestContext.TestName;
            account[CrmRelationshipType] = new OptionSetValue((int)RelationshipType.Demo);
            account[CrmPlatformAccess] = true;
            _accounts.Add(account);

            using (var crmService = new OrganizationService(CrmConnection.Parse(ConnectionString)))
                {
                    var managersQuery = new QueryByAttribute(CrmManager);
                managersQuery.AddAttributeValue(CrmManagerEmail, CrmTestUser);
                var managers = crmService.RetrieveMultiple(managersQuery).Entities;
                _crmTestUserId = managers.Count > 0 ? managers[0].Id : Guid.Empty;

                _insertionDate = DateTime.UtcNow;

                    _accounts.ForEach(a => crmService.Create(a));

                    if (TestContext.TestName == "ListChangedAccounts")
                    {
                        _offices.ForEach(o => crmService.Create(o));

                        for (var i = 0; i < _accounts.Count; i++)
                        {
                            crmService.Associate(CrmAccount,
                                                    _accounts[i].Id,
                                                    new Relationship(CrmAccountOfficeRelationship),
                                                new EntityReferenceCollection(new[] { new EntityReference(CrmOffice, _offices[i].Id) }));

                        if (_crmTestUserId != Guid.Empty)
                        {
                            crmService.Associate(CrmAccount,
                                                    _accounts[i].Id,
                                                    new Relationship(CrmAccountManagerRelationship),
                                                    new EntityReferenceCollection(new[] { new EntityReference(CrmManager, _crmTestUserId) }));
                        }
                    }                    
                }
            }
        }

        private void CreateTestContacts()
        {
            _contacts = new List<CrmEntity>();

            var contact1 = new CrmEntity(CrmContactInfo.EntityName) { Id = Guid.NewGuid() };
            contact1[CrmContactInfo.FirstName] = "1";
            contact1[CrmContactInfo.IsPlatformUser] = true;
            contact1[CrmContactInfo.StateCode] = new OptionSetValue(0);  // 0-> active
            contact1[CrmContactInfo.Email] = "mihai.manta@sustainalytics.com" + Guid.NewGuid().ToString() ;

            _contacts.Add(contact1);

            var contact2 = new CrmEntity(CrmContactInfo.EntityName) { Id = Guid.NewGuid() };
            contact2[CrmContactInfo.FirstName] = "2";
            contact2[CrmContactInfo.IsPlatformUser] = false;
            contact2[CrmContactInfo.StateCode] = new OptionSetValue(1);  // 0-> active
            contact2[CrmContactInfo.Email] = "mihai.manta@sustainalytics.com" + Guid.NewGuid().ToString();

            _contacts.Add(contact2);


            var prospectAccount = _accounts.FirstOrDefault((a) => a[CrmRelationshipType].Equals(new OptionSetValue((int)RelationshipType.Prospect)));
            if (prospectAccount == null)
                throw new Exception("a prospect account is expected");


            var clientAccount = _accounts.FirstOrDefault((a) => a[CrmRelationshipType].Equals(new OptionSetValue((int)RelationshipType.Client)));
            if (clientAccount == null)
                throw new Exception("a client account is expected");


            using (var crmService = new OrganizationService(CrmConnection.Parse(ConnectionString)))
            {

                _contacts.ForEach(a => crmService.Create(a));


                crmService.Associate(CrmContactInfo.EntityName,
                                     contact1.Id,
                                     new Relationship("contact_customer_accounts"),
                                     new EntityReferenceCollection(new[] { new EntityReference(CrmAccount, prospectAccount.Id) }));



                crmService.Associate(CrmContactInfo.EntityName,
                                     contact2.Id,
                                     new Relationship("contact_customer_accounts"),
                                     new EntityReferenceCollection(new[] { new EntityReference(CrmAccount, clientAccount.Id) }));


            }
        }



        private void DeleteTestAccounts()
        {
            using (var crmService = new OrganizationService(CrmConnection.Parse(ConnectionString)))
            {
                    _accounts.ForEach(a => crmService.Delete(a.LogicalName, a.Id));

                    if (TestContext.TestName == "ListChangedAccounts")
                    {
                        _offices.ForEach(o => crmService.Delete(o.LogicalName, o.Id));
                    }


            }
        }

        private void DeleteTestContacts()
        {
            using (var crmService = new OrganizationService(CrmConnection.Parse(ConnectionString)))
            {
                _contacts.ForEach(a => crmService.Delete(a.LogicalName, a.Id));
            }
        }


        private void RemoveManagerFromAccounts(IEnumerable<Guid> accountsIds)
        {
            if (_crmTestUserId != Guid.Empty)
            {
                using (var crmService = new OrganizationService(CrmConnection.Parse(ConnectionString)))
                {
                    foreach (var aId in accountsIds)
                    {
                        crmService.Disassociate(CrmAccount,
                                                aId,
                                                new Relationship(CrmAccountManagerRelationship),
                                                new EntityReferenceCollection(new[] { new EntityReference(CrmManager, _crmTestUserId) }));   
                    }
                }
            }
        }

        private void ModifyCrmEntities(List<CrmEntity> crmEntities)
        {
            using (var crmService = new OrganizationService(CrmConnection.Parse(ConnectionString)))
            {
                crmEntities.ForEach(o =>
                {
                    o[CrmName] += "_MODIFIED";

                    crmService.Update(o);
                });
            }
        }

        [TestMethod]
        public void ListAccountsIds()
        {
            var crmGateway = DynamicsCrmGateway.GetInstance(ConnectionString);

            var accountsIds = crmGateway.ListAccountsIds();

            Assert.IsTrue(_accounts.Any(a => accountsIds.Contains(a.Id)), "Not all test accounts were found in the list provided by the CRM gateway!");
        }

        [TestMethod]
        public void ListChangedAccounts()
        {
            var crmGateway = DynamicsCrmGateway.GetInstance(ConnectionString);

            var changedAccountsAfterInsertion = crmGateway.ListChangedAccounts(_insertionDate + crmGateway.GetCrmTimeOffset());

            Thread.Sleep(TimeSpan.FromSeconds(1));

            var managerRemovalTime = DateTime.UtcNow;

            var accountWithoutManager = _accounts[0];

            RemoveManagerFromAccounts(new [] { accountWithoutManager.Id });

            var crmOffset =  crmGateway.GetCrmTimeOffset();
            var removalTime = managerRemovalTime + crmOffset;
            var changedAccountsAfterManagerRemoval = crmGateway.ListChangedAccounts(removalTime);

            var crmAccountWithoutManager = changedAccountsAfterManagerRemoval.SingleOrDefault(a => a.Id == accountWithoutManager.Id);

            Thread.Sleep(TimeSpan.FromSeconds(1));

            var officeChangeTime = DateTime.UtcNow;

            const int indexOfAccountWithModifiedOffice = 1;

            ModifyCrmEntities(new List<CrmEntity> { _offices[indexOfAccountWithModifiedOffice] });

            var changedAccountsAfterOfficeChange = crmGateway.ListChangedAccounts(officeChangeTime + crmGateway.GetCrmTimeOffset());

            var crmAccountWithChangedOffice = changedAccountsAfterOfficeChange.SingleOrDefault(a => a.Id == _accounts[indexOfAccountWithModifiedOffice].Id);

            Thread.Sleep(TimeSpan.FromSeconds(1));

            var accountChangeTime = DateTime.UtcNow;

            const int indexOfModifiedAccount = 2;

            ModifyCrmEntities(new List<CrmEntity> { _accounts[indexOfModifiedAccount] });

            var serverAccountChangedTime = accountChangeTime + crmGateway.GetCrmTimeOffset();
            var changedAccounts = crmGateway.ListChangedAccounts(serverAccountChangedTime);

            var crmChangedAccount = changedAccounts.SingleOrDefault(a => a.Id == _accounts[indexOfModifiedAccount].Id);

            Assert.IsTrue(_accounts.Any(a => changedAccountsAfterInsertion.Any(ca => ca.Id == a.Id)), "Not all newly created accounts were listed!");
            Assert.IsTrue(_accounts.Any(a => a.Id != accountWithoutManager.Id && changedAccountsAfterManagerRemoval.All(ca => ca.Id != a.Id)), "Originally created accounts still appear as being modified!");
            Assert.IsNotNull(crmAccountWithoutManager, "The account which lost its manager does not appear as modified!");
            Assert.IsNull(crmAccountWithoutManager.AccountManager, "The manager was not actually removed in CRM as it should have been!");
            Assert.IsTrue(_accounts.Any(a => a.Id != _accounts[indexOfAccountWithModifiedOffice].Id && changedAccountsAfterOfficeChange.All(ca => ca.Id != a.Id)), "Originally created accounts still appear as being modified!");
            Assert.IsNotNull(crmAccountWithChangedOffice, "The account which changed its office does not appear as modified!");
            Assert.IsTrue(crmAccountWithChangedOffice.Office == _offices[indexOfAccountWithModifiedOffice][CrmName].ToString(), "The new office does not appear in the CRM entity!");
            Assert.IsTrue(_accounts.Any(a => a.Id != _accounts[indexOfModifiedAccount].Id && changedAccounts.All(ca => ca.Id != a.Id)), "Originally created accounts still appear as being modified!");
            Assert.IsNotNull(crmChangedAccount, "The changed account does not appear as modified!");
            Assert.IsTrue(crmChangedAccount.Name == _accounts[indexOfModifiedAccount][CrmName].ToString(), "The new name of the modified account does not appear in the CRM entity!");
        }

        [TestMethod]
        public void ListContactsIds()
        {
            var crmGateway = DynamicsCrmGateway.GetInstance(ConnectionString);

            var contactsIds = crmGateway.ListContactsIds();

            Assert.IsTrue(_contacts.All(a => contactsIds.Contains(a.Id)), "Not all test contacts were found in the list provided by the CRM gateway!");
        }

        [TestMethod]
        public void ListChangedContacts()
        {
            var crmGateway = DynamicsCrmGateway.GetInstance(ConnectionString);
            var crmTimeOffset = crmGateway.GetCrmTimeOffset();


            var changedContactsAfterInsertion = crmGateway.ListChangedContacts(_insertionDate + crmTimeOffset);

         

            DateTime now = DateTime.UtcNow + crmTimeOffset;
            var modifiedContact = _contacts[0];
            using (var crmService = new OrganizationService(CrmConnection.Parse(ConnectionString)))
            {
                modifiedContact[CrmContactInfo.LastName] = "new name " +  now;
                crmService.Update(modifiedContact);
            }

            Thread.Sleep(1);
            var crmContacts = crmGateway.ListChangedContacts(now);
            var fetchedContact = crmContacts.FirstOrDefault((c) => c.Id == modifiedContact.Id);

            Assert.IsTrue(_contacts.All(a => changedContactsAfterInsertion.Any(c => c.Id == a.Id)), "Not all created contacts were found in the list provided by the CRM gateway!");
            Assert.IsNotNull(fetchedContact, "the modiefied contact is not found in the list provided by the CRM gateway!");
            Assert.AreEqual(modifiedContact[CrmContactInfo.LastName], fetchedContact.LastName);
        }


        [TestMethod]
        public void ListContactsByAccount()
        {
            var crmGateway = DynamicsCrmGateway.GetInstance(ConnectionString);
            var prospectAccount = _accounts.FirstOrDefault((a) => a[CrmRelationshipType].Equals(new OptionSetValue((int)RelationshipType.Prospect)));
            var associatedContact = _contacts[0];

            Assert.IsNotNull(prospectAccount, "initialization data are not as expected");

            var crmContacts = crmGateway.ListContactsByAccountId(prospectAccount.Id);
            Assert.IsNotNull(crmContacts.FirstOrDefault(a => associatedContact.Id == a.Id), "ListContactsByAccountId should return the associated contact");
            
        }


        [TestMethod]
        public void CRM_Schema_Validation()
        {
            var crmGateway = DynamicsCrmGateway.GetInstance(ConnectionString);
            var result  = crmGateway.ValidateSchema();
            Assert.AreEqual(true, result.IsValid, string.Format("crm schema is not as expected: {0}", result));
            
        }
    }
}
